package demo.java.net;

import com.alibaba.fastjson.JSON;
import demo.vo.http.ResponseVo;
import org.apache.commons.lang.StringUtils;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.ws.rs.core.MediaType;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import static demo.java.lang.StringDemo.UTF8;

public class HttpDemo {

    private static Logger logger = LoggerFactory.getLogger(HttpDemo.class);

    @Test
    public void testOctetStream() {
        String url = "http://xdcs.ximalaya.com/log-proxy/gaea/download" +
                "?ip=10.192.5.19" +
                "&dir=/var/log/gaea-console/container-gaea-console-7498b6485c-6fp5j/gaea-console/" +
                "&file=springboot-start.log";
        try {
            ResponseVo vo = get(url, 3000, 6000);
            System.out.println("ContentType = " + vo.getContentType());
            System.out.println("Content = " + vo.getContent());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    @Test
    public void testXdcsHealthCheck2XX() {
        String url = "http://192.168.3.52:8480/cmdb-web/healthcheck";
        try {
            ResponseVo httpResult = get(url, 1000, 3000);
            logger.info("response = {}", httpResult);
        } catch (IOException e) {
            logger.error("", e);
        }
//        try {
//            httpGet(url);
//        } catch (IOException e) {
//            e.printStackTrace();
//        }
    }


    @Test
    public void testXdcsHealthCheck4XX() {
        String url = "http://192.168.3.52:8480/cmdb-web/healthcheck?logLevelOperate=DEBUG&level=DEBUG&class=com.ximalaya.hera.home.outer.DefaultWoodpeckerService&&logLevelOperateSecretKey=4o36861bbcg138584bbb6c2e0c97b6t3";
        try {
            ResponseVo httpResult = get(url, 1000, 3000);
            logger.info("response = {}", httpResult);
        } catch (IOException e) {
            logger.error("", e);
        }
//        try {
//            httpGet(url);
//        } catch (IOException e) {
//            e.printStackTrace();
//        }
    }

    /**
     * 查询Flume的Metric信息
     */
    @Test
    public void flumeMetrics() {
        String url = "http://192.168.3.52:22241/metrics";
        try {
            ResponseVo httpResult = requestAsText(url, null, "GET");
            logger.info("response = {}", httpResult);
        } catch (IOException e) {
            logger.error("", e);
        }
    }

    /**
     * 检查war包更新时间
     */
    @Test
    public void yunweiCheckout() {
        String url = "http://op3.ximalaya.com/yunwei/checkut";
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("appname", "cmdb-web    ");
        try {
            ResponseVo httpResult = requestAsForm(url, map, RequestMethod.POST);
            logger.info("response = {}", httpResult);
        } catch (IOException e) {
            logger.error("", e);
        }
    }

    /**
     * 健康检查，IO异常或者非2xx时重试
     *
     * @param url
     * @param retry (如果retry<=0，则至少请求一次；如果retry>0，则最多请求 retry+1 次)
     * @return
     * @throws IOException
     */
    public static ResponseVo retryGet(String url, int retry) throws IOException {
        long t0 = System.currentTimeMillis();
        while (retry > 0) {
            retry--;
            try {
                ResponseVo vo = get(url, 2000, 5000);
                if (vo != null && vo.isHttpStatus2XX()) {
                    vo.setCost(System.currentTimeMillis() - t0);
                    return vo;
                }
            } catch (IOException e) {
                logger.warn("request {} failed: {}", url, e.getMessage());
            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
            }
        }
        ResponseVo vo = get(url, 2000, 5000);
        vo.setCost(System.currentTimeMillis() - t0);
        return vo;
    }

    /**
     * 获取response中指定的字符集
     *
     * @param conn
     * @return
     */
    public static String getResponseCharSet(HttpURLConnection conn) {
        if (conn == null) {
            return null;
        }
        String contentType = conn.getContentType();
        if (StringUtils.isBlank(contentType)) {
            return null;
        }
        String[] values = contentType.split(";");
        String charset = null;
        for (String value : values) {
            int pos = value.toLowerCase().indexOf("charset=");
            if (pos >= 0) {
                charset = value.substring(pos + "charset=".length()).trim();
                break;
            }
        }
        return charset;
    }

    /**
     * http JSON请求
     *
     * @param url
     * @param body
     * @param method
     * @return
     * @throws IOException
     */
    public static ResponseVo requestAsJson(String url, Object body, RequestMethod method) throws IOException {
        Map<String, String> requestPropMap = new HashMap<String, String>();
        requestPropMap.put("Content-Type", "application/json;charset=" + StandardCharsets.UTF_8);
        return httpRequest(url, JSON.toJSONString(body), method, requestPropMap, 3000, 9000);
    }

    /**
     * http请求
     *
     * @param url
     * @param body
     * @param method
     * @param cookie
     * @return
     * @throws IOException
     */
    public static ResponseVo requestAsText(String url, String body, RequestMethod method, String cookie)
            throws IOException {
        Map<String, String> requestPropMap = new HashMap<String, String>();
        requestPropMap.put("Content-Type", "text/html;charset=" + StandardCharsets.UTF_8);
        requestPropMap.put("Cookie", cookie);
        return httpRequest(url, body, method, requestPropMap, 3000, 9000);
    }

    /**
     * http请求
     *
     * @param url
     * @param body
     * @param method
     * @return
     * @throws IOException
     */
    public static ResponseVo requestAsText(String url, String body, String method) throws IOException {
        Map<String, String> requestPropMap = new HashMap<String, String>();
        requestPropMap.put("Content-Type", "text/html;charset=" + StandardCharsets.UTF_8);
        return httpRequest(url, body, method, requestPropMap, 3000, 9000);
    }

    /**
     * http请求
     *
     * @param url
     * @param method
     * @return
     * @throws IOException
     */
    public static ResponseVo request(String url, String method) throws IOException {
        return httpRequest(url, null, method, null, 3000, 9000);
    }

    /**
     * GET请求
     *
     * @param url
     * @param connectTimeout
     * @param readTimeout
     * @return
     * @throws IOException
     */
    public static ResponseVo get(String url, int connectTimeout, int readTimeout) throws IOException {
        return httpRequest(url, null, "GET", null, connectTimeout, readTimeout);
    }

    /**
     * FORM请求
     *
     * @param url
     * @param form
     * @param method
     * @return
     * @throws IOException
     */
    public static ResponseVo requestAsForm(String url, Map<String, Object> form, RequestMethod method)
            throws IOException {
        Map<String, String> requestPropMap = new HashMap<String, String>();
        requestPropMap.put("Content-Type", "application/x-www-form-urlencoded;charset=" + StandardCharsets.UTF_8);
        StringBuilder body = new StringBuilder();
        if (form != null && !form.isEmpty()) {
            for (Entry<String, Object> entry : form.entrySet()) {
                body.append(entry.getKey()).append("=")
                        .append(URLEncoder.encode(String.valueOf(entry.getValue()), StandardCharsets.UTF_8.name()))
                        .append("&");
            }
            body.deleteCharAt(body.length() - 1);
        }
        return httpRequest(url, body.toString(), method, requestPropMap, 3000, 9000);
    }

    /**
     * form 表单数据
     *
     * @param form
     * @return
     * @throws UnsupportedEncodingException
     */
    public static String mapFormBody(Map<String, Object> form) throws UnsupportedEncodingException {
        StringBuilder body = new StringBuilder();
        if (form != null && !form.isEmpty()) {
            for (Entry<String, Object> entry : form.entrySet()) {
                body.append(entry.getKey()).append("=")
                        .append(URLEncoder.encode(String.valueOf(entry.getValue()), UTF8)).append("&");
            }
            body.deleteCharAt(body.length() - 1);
        }
        return body.toString();
    }

    /**
     * http请求
     *
     * @param url
     * @param body
     * @param method            GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
     * @param requestProperties
     * @param connectTimeout
     * @param readTimeout
     * @return
     * @throws IOException
     */
    public static ResponseVo httpRequest(String url, String body, RequestMethod method,
                                         Map<String, String> requestProperties, int connectTimeout, int readTimeout) throws IOException {
        return httpRequest(url, body, method.name(), requestProperties, connectTimeout, readTimeout);
    }

    /**
     * http请求
     *
     * @param url
     * @param body
     * @param method            GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
     * @param requestProperties
     * @param connectTimeout
     * @param readTimeout
     * @return
     * @throws IOException
     */
    public static ResponseVo httpRequest(String url, String body, String method, Map<String, String> requestProperties,
                                         int connectTimeout, int readTimeout) throws IOException {
        long t0 = System.currentTimeMillis();
        URL urlObject = new URL(url);
        HttpURLConnection httpUrlConnection = (HttpURLConnection) urlObject.openConnection();
        HttpURLConnection.setFollowRedirects(true);
        /**
         * 设置是否向 httpUrlConnection 输出， 对于post请求，参数要放在 HTTP 正文内，因此需要设为true。 默认情况下是false;
         * setDoOutput() 方法是为了下面 getOutputStream()；
         */
        httpUrlConnection.setDoOutput(true);
        // 置是否从 httpUrlConnection 读入，默认情况下是true; setDoInput() 方法是为了下面 getInputStream()。
        httpUrlConnection.setDoInput(true);
        // 连接超时设置
        httpUrlConnection.setConnectTimeout(connectTimeout);
        // 读取超时设置
        httpUrlConnection.setReadTimeout(readTimeout);
        // 请求方法设置
        httpUrlConnection.setRequestMethod(method);
        // 是否使用缓存
        httpUrlConnection.setUseCaches(false);
        if (requestProperties != null && !requestProperties.isEmpty()) {
            for (Entry<String, String> entry : requestProperties.entrySet()) {
                httpUrlConnection.setRequestProperty(entry.getKey(), entry.getValue());
            }
        }
        /**
         * 1.Accept属于请求头， Content-Type属于实体头。Http报头分为通用报头，请求报头，响应报头和实体报头。
         * <li>请求方的http报头结构：通用报头|请求报头|实体报头
         * <li>响应方的http报头结构：通用报头|响应报头|实体报头
         * <p>
         * <li>Accept代表发送端（客户端）希望接受的数据类型。 比如：Accept：text/xml（application/json）;
         * 代表客户端希望接受的数据类型是xml（json ）类型
         * <li>Content-Type代表发送端（客户端|服务器）发送的实体数据的数据类型。
         * 比如：Content-Type：text/html（application/json） ; 代表发送端发送的数据格式是html（json）。
         */
        httpUrlConnection.setRequestProperty("Accept", MediaType.WILDCARD);
        /**
         * 这会与服务器建立 Socket 连接，而连接以后，连接属性就不可以再修改； 但是可以查询服务器返回的头信息了（header information）。
         */
        httpUrlConnection.connect();
        if (body != null && !body.isEmpty() && ("POST".equals(method) || "PUT".equals(method))) {
            try (OutputStream outputStream = httpUrlConnection.getOutputStream();) {
                outputStream.write(body.getBytes(StandardCharsets.UTF_8));
                outputStream.flush();
            }
        }
        int responseCode = httpUrlConnection.getResponseCode();
        /**
         * 如：Content-Length: 62
         * <p>
         * Content-Length用于描述HTTP消息实体的传输长度the transfer-length of the message-body。
         * 在HTTP协议中，消息实体长度和消息实体的传输长度是有区别，比如说gzip压缩下，消息实体长度是压缩前的长度，消息实体的传输长度是gzip压缩后的长度。
         * 在具体的HTTP交互中，客户端是如何获取消息长度的呢，主要基于以下几个规则：
         *
         * <li>响应为1xx，204，304相应或者head请求，则直接忽视掉消息实体内容。
         * <li>如果有Transfer-Encoding，则优先采用Transfer-Encoding里面的方法来找到对应的长度。比如说Chunked模式。
         * “如果head中有Content-Length，那么这个Content-Length既表示实体长度，又表示传输长度。如果实体长度和传输长度不相等(比如说设置了Transfer-Encoding)，那么则不能设置Content-Length。
         * 如果设置了Transfer-Encoding，那么Content-Length将被忽视”。这句话翻译的优点饶，其实关键就一点：有了Transfer-Encoding，则不能有Content-Length。
         * <li>通过服务器关闭连接能确定消息的传输长度。(请求端不能通过关闭连接来指明请求消息体的结束，因为这样可以让服务器没有机会继续给予响应)。
         * 这种情况主要对应为短连接，即非keep-alive模式。
         * <li>HTTP1.1必须支持chunk模式。因为当不确定消息长度的时候，可以通过chunk机制来处理这种情况。
         * <li>在包含消息内容的header中，如果有content-length字段，那么该字段对应的值必须完全和消息主题里面的长度匹配。 “The
         * entity-length of a message is the length of the message-body before any
         * transfer-codings have been applied” 也就是有chunk就不能有content-length 。
         * <p>
         * 其实后面几条几乎可以忽视，简单总结后如下：
         * <li>1、Content-Length如果存在并且有效的话，则必须和消息内容的传输长度完全一致。(经过测试，如果过短则会截断，过长则会导致超时。)
         * <li>2、如果存在Transfer-Encoding(重点是chunked)，则在header中不能有Content-Length，有也会被忽视。
         * <li>3、如果采用短连接，则直接可以通过服务器关闭连接来确定消息的传输长度。(这个很容易懂)
         */
        int contentLength = httpUrlConnection.getContentLength();
        // 如：Date: Tue, 09 Feb 2021 09:41:29 GMT
        long date = httpUrlConnection.getDate();
        // 如：Content-Type: text/plain;charset=UTF-8
        String contentType = httpUrlConnection.getContentType();
        if (contentLength == 0) {
            ResponseVo vo = new ResponseVo(responseCode, contentType, contentLength, date, "");
            vo.setCost(System.currentTimeMillis() - t0);
            vo.setMethod(method);
            vo.setUrl(url);
            return vo;
        }

        StringBuilder data = new StringBuilder();
        InputStream responseInputStream;
        if (responseCode < HttpURLConnection.HTTP_BAD_REQUEST) {
            responseInputStream = httpUrlConnection.getInputStream();
        } else {
            responseInputStream = httpUrlConnection.getErrorStream();
        }
        String charset = getResponseCharSet(httpUrlConnection);
        charset = (charset == null || charset.isEmpty()) ? StandardCharsets.UTF_8.name() : charset;
        try (InputStreamReader inStreamReader = new InputStreamReader(responseInputStream, charset);
             BufferedReader bufferedReader = new BufferedReader(inStreamReader);) {
            final char[] cbuf = new char[1024];
            int readLength;
            while ((readLength = bufferedReader.read(cbuf)) != -1) {
                data.append(cbuf, 0, readLength);
            }
            /**
             * 是否需要关闭，应该根据实际需要来。
             * <li>当 HttpURLConnection 是 "Connection: close " 模式，那么关闭 inputStream 后就会自动断开连接。
             * <li>当 HttpURLConnection 是 "Connection: Keep-Alive" 模式，那么关闭
             * inputStream后，并不会断开底层的 Socket 连接。 这样的好处，是当需要连接到同一服务器地址时，可以复用该
             * Socket。这时如果要求断开连接，就可以调用 connection.disconnect() 了。
             * <p>
             * HttpURLConnection 连接到底是不是 Keep-Alive 模式，除了 HttpURLConnection 请求设置为 Keep-Alive
             * 外 （http 1.0中默认是关闭的，http 1.1中默认启用Keep-Alive），也需要服务器支持 Keep-Alive，才可以真正建立
             * Keep-Alive 连接。
             *
             */
            httpUrlConnection.disconnect();
        }
        ResponseVo vo = new ResponseVo(responseCode, contentType, contentLength, date, data.toString());
        vo.setCost(System.currentTimeMillis() - t0);
        vo.setMethod(method);
        vo.setUrl(url);
        return vo;
    }


}
